using System;
using Checkpoint2XNA.Models;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Checkpoint2XNA.Utilities;
using System.Threading.Tasks;

namespace Checkpoint2XNA
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class ShinyChess : Microsoft.Xna.Framework.Game
    {
        private const int MAX_RECURSIVE_RAY_DEPTH = 2;

        private KeyboardState oldKeyState;
        private MouseState oldMouseState;

        private Model3D selectedPiece;

        private Texture2D canvas;
        private Rectangle tracedSize;
        private float inverseHeight;
        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private UInt32[] pixels;

        private Vector3[] cameraRays;

        private LightSC light;

        private Vector3 cameraPoint;
        private Vector3 cameraDirection;
        private Vector3 cameraUp;
        private Vector3 cameraRight;

        private Vector3 virtScreenPoint;
        private Vector3 virtScreenDimensions;
        private float fovX;
        private float fovY;


        public ShinyChess()
        {
            oldKeyState = new KeyboardState();
            oldMouseState = new MouseState();

            graphics = new GraphicsDeviceManager(this);

            //graphics.PreferredBackBufferWidth = 576;
            //graphics.PreferredBackBufferHeight = 442;
            //graphics.PreferredBackBufferWidth = 288;
            //graphics.PreferredBackBufferHeight = 221;
            graphics.PreferredBackBufferWidth = 300;
            graphics.PreferredBackBufferHeight = 300;

            Content.RootDirectory = "Content";

            Components.Add(new FrameRateCounter(this));
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            this.IsMouseVisible = true;
            
            tracedSize = GraphicsDevice.PresentationParameters.Bounds;
            canvas = new Texture2D(GraphicsDevice, tracedSize.Width, tracedSize.Height, false, SurfaceFormat.Color);
            pixels = new UInt32[tracedSize.Width * tracedSize.Height];
            
            inverseHeight = 1.0f / (float)tracedSize.Height;

            fovX = (float)Math.PI / (float)4;
            fovY = (float)tracedSize.Height / (float)tracedSize.Width * fovX;

            cameraPoint = new Vector3(0, 0, 0);
            cameraDirection = new Vector3(0, -.5f, -.5f);
            cameraDirection.Normalize();
            cameraUp = new Vector3(0, .5f, -.5f);
            cameraUp.Normalize();
            cameraRight = Vector3.Cross(cameraDirection, cameraUp);
            cameraRight.Normalize();
            cameraUp = Vector3.Cross(cameraRight, cameraDirection);
            cameraUp.Normalize();

            cameraRays = new Vector3[tracedSize.Height * tracedSize.Width];
            ComputeCameraRays();


            double aspect = (double)tracedSize.Width / (double)tracedSize.Height;
            virtScreenDimensions = new Vector3(1 * (float)aspect, 1, 0);
            virtScreenPoint = new Vector3(-virtScreenDimensions.X / 2, -virtScreenDimensions.Y / 2, -.5f);


            ChessModelManager.InitializePieces();

            
            light = new LightSC(new Vector3(-3f, 1000, -23.3f), Color.White, .8f);

            base.Initialize();
        }

        private void ComputeCameraRays()
        {
            for (int x = 0; x < tracedSize.Width; x++)
            {
                for (int y = 0; y < tracedSize.Height; y++)
                {
                    //Vector3 ray = new Vector3((float)(2 * x - tracedSize.Width) / (float)tracedSize.Width * (float)FastTan(fovX / 2),
                    //                            (float)(2 * y - tracedSize.Height) / (float)tracedSize.Height * (float)FastTan(fovY / 2),
                    //                            (1 + cameraPoint.Z));
                    //ray.Normalize();

                    float normalized_i = (x / (float)tracedSize.Width) - 0.5f;
                    float normalized_j = (y / (float)tracedSize.Height) - 0.5f;
                    Vector3 image_point = Vector3.Multiply(cameraRight, normalized_i) +
                                          Vector3.Multiply(cameraUp, normalized_j) +
                                          cameraPoint + cameraDirection;
                    Vector3 ray_direction = image_point - cameraPoint;
                    ray_direction.Normalize();
                    
                    cameraRays[tracedSize.Height * y + x] = ray_direction;
                }
            }
        }


        private Vector3 GetCameraRayWithMouseCoordinates(int x, int y)
        {
            return cameraRays[tracedSize.Height * (tracedSize.Height - y) + x];
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            var newKeyState = Keyboard.GetState();

            if (newKeyState.IsKeyDown(Keys.R))
            {
                if (!oldKeyState.IsKeyDown(Keys.R))
                {
                    ChessModelManager.InitializePieces();
                }
            }

            oldKeyState = newKeyState;

            var newMouseState = Mouse.GetState();

            if (!ChessModelManager.animating)
            {
                if (newMouseState.LeftButton == ButtonState.Pressed)
                {
                    if (oldMouseState.LeftButton == ButtonState.Released)
                    {
                        var x = newMouseState.X;
                        var y = newMouseState.Y;
                        if (x >= 0 && y >= 0)
                        {
                            var ray = GetCameraRayWithMouseCoordinates(x, y);
                            Tuple<Model3D, double> response = ChessModelManager.RayCollide(cameraPoint, ray);
                            if (response != null)
                            {
                                var plane = (response.Item1 as CheckerPlane);

                                if (selectedPiece == null && plane == null)
                                {
                                    SelectPiece(response.Item1);
                                }
                                else if (selectedPiece != null)
                                {
                                    if (plane != null)
                                    {
                                        var clickedPoint = new Vector3((float)(ray.X * response.Item2 + cameraPoint.X),
                                                                         (float)(ray.Y * response.Item2 + cameraPoint.Y),
                                                                         (float)(ray.Z * response.Item2 + cameraPoint.Z));
                                        var gridIndex = ChessModelManager.GetGridIndexByPoint(clickedPoint);
                                        ChessModelManager.MovePiece(selectedPiece, gridIndex);
                                        DeselectPiece();
                                    }
                                    else if (response.Item1.Side == selectedPiece.Side)
                                    {
                                        SelectPiece(response.Item1);
                                    }
                                    else
                                    {
                                        var clickedPoint = new Vector3((float)(ray.X * response.Item2 + cameraPoint.X),
                                                                         (float)(ray.Y * response.Item2 + cameraPoint.Y),
                                                                         (float)(ray.Z * response.Item2 + cameraPoint.Z));
                                        var gridIndex = ChessModelManager.GetGridIndexByPoint(clickedPoint);
                                        ChessModelManager.MovePiece(selectedPiece, gridIndex);
                                        DeselectPiece();
                                    }
                                }
                            }
                            else
                                Console.WriteLine("Nothing.");
                        }
                    }
                }
            }

            oldMouseState = newMouseState;

            ChessModelManager.Update(gameTime);

            base.Update(gameTime);
        }


        public void SelectPiece(Model3D model)
        {
            DeselectPiece();
            selectedPiece = model;
            selectedPiece.Color = Color.Gold;
        }

        public void DeselectPiece()
        {
            if (selectedPiece != null)
            {
                selectedPiece.Color = selectedPiece.Side == ChessSide.Black ? Color.DarkRed : Color.Silver;
                selectedPiece = null;
            }
        }


        public double FastTan(double x)
        {
            if (x < -3)
                return -1;
            else if (x > 3)
                return 1;
            else
                return x * (27 + x * x) / (27 + 9 * x * x);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            GraphicsDevice.Textures[0] = null;

            //Ray Trace
            //for(int i = 0; i < cameraRays.Length; i++)
            Parallel.For(0, cameraRays.Length, i =>
            {
                int x = i % tracedSize.Height;
                int y = (int)(i * inverseHeight);
                Vector3 ray = cameraRays[i];

                pixels[x + (tracedSize.Height - y - 1) * tracedSize.Width] = Shade(cameraPoint, ray, 0).PackedValue;                   
            });                 

            canvas.SetData<UInt32>(pixels, 0, tracedSize.Width * tracedSize.Height);

            spriteBatch.Begin();
            spriteBatch.Draw(canvas, new Rectangle(0, 0, tracedSize.Width, tracedSize.Height), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }


        public Color Shade(Vector3 origin, Vector3 ray, int depth)
        {
            Tuple<Model3D, double> result = ChessModelManager.RayCollide(cameraPoint, ray);

            if (result != null)
            {
                var collision = new Vector3(ray.X * (float)result.Item2 + origin.X, ray.Y * (float)result.Item2 + origin.Y, ray.Z * (float)result.Item2 + origin.Z);
                var normal = result.Item1.GetNormal(collision);
                collision += Vector3.Multiply(normal, .0001f);


                Vector3 diffuseComponent = new Vector3();
                Vector3 specularComponent = new Vector3();

                                
                //bool hitLight = true;

                var L = light.Position - collision;
                L.Normalize();

                //disable shadow rays
                //if (ChessModelManager.RayCollide(collision, L) != null)
                //{
                //   hitLight = false;
                //}

                //if (hitLight)
                //{
                    diffuseComponent += light.Intensity * light.Color.ToVector3() * Vector3.Dot(L, normal);
                    Vector3 R = Vector3.Reflect(-L, normal);
                    specularComponent += light.Intensity * light.Color.ToVector3() * Vector3.Dot(R, ray);
                //}

                Vector3 localColor = result.Item1.GetColorAt(collision).ToVector3() *
                                    (U.AMBIENT_COEF * U.AmbientColor.ToVector3() +
                                    (U.DIFFUSE_COEF * diffuseComponent));
                localColor.X += (float)Math.Pow(U.SPECULAR_COEF * specularComponent.X, 6);
                localColor.Y += (float)Math.Pow(U.SPECULAR_COEF * specularComponent.Y, 6);
                localColor.Z += (float)Math.Pow(U.SPECULAR_COEF * specularComponent.Z, 6);

                //reflect rays
                if (depth < MAX_RECURSIVE_RAY_DEPTH && result.Item1.ReflectionMod > 0)
                {
                    localColor = localColor + result.Item1.ReflectionMod * Shade(collision, ray - 2 * normal * (Vector3.Dot(ray, normal)), depth + 1).ToVector3();
                }


                return new Color(localColor);
            }
            else
            {
                return Color.CornflowerBlue;
            }
        }//end shade
    }
}
